import base64
import re
import uuid
import adbutils
import xml
from Driver.BaseDriver import BaseDriver, check_status
from Android import Device, Initer
from Common import AndroidLocator
from typing import Union


def parse_bounds(text):
    m = re.match(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]', text)
    if m is None:
        return None
    (lx, ly, rx, ry) = map(int, m.groups())
    return dict(x=lx, y=ly, width=rx - lx, height=ry - ly)


def safe_xmlstr(s):
    return s.replace("$", "-")


def str2bool(v):
    return v.lower() in ("yes", "true", "t", "1")


def str2int(v):
    return int(v)


def convstr(v):
    return v


alias = {
    'class': '_type',
    'resource-id': 'resourceId',
    'content-desc': 'description',
    'long-clickable': 'longClickable',
    'bounds': 'rect',
}

parsers = {
    '_type': safe_xmlstr, # node className
    # Android
    'rect': parse_bounds,
    'text': convstr,
    'resourceId': convstr,
    'package': convstr,
    'checkable': str2bool,
    'scrollable': str2bool,
    'focused': str2bool,
    'clickable': str2bool,
    'selected': str2bool,
    'longClickable': str2bool,
    'focusable': str2bool,
    'password': str2bool,
    'index': int,
    'description': convstr,
    # iOS
    'name': convstr,
    'label': convstr,
    'x': str2int,
    'y': str2int,
    'width': str2int,
    'height': str2int,
    # iOS && Android
    'enabled': str2bool,
}


class AndroidDriver(BaseDriver):

    def __init__(self, serial: str):
        self._platform = 'android'
        self._serial = serial
        self._client: Device = None
        self._init = False
        self.status = 'disconnect'

    ###########################################################################
    # 设备方法
    ###########################################################################
    def device_connect(self):
        """
        连接设备
        :return:
        """
        initer = Initer(adbutils.AdbDevice(adbutils.adb, self._serial))
        if initer.check_install():
            self._init = True
            initer.start_atx_agent()
        else:
            initer.install()
        self._client = Device(self._serial)
        self._client.healthcheck()
        self._client.set_fastinput_ime(True)
        self.status = 'connect'

    def device_disconnect(self):
        """
        断开连接
        :return:
        """
        if self._init is False:
            Initer(adbutils.AdbDevice(adbutils.adb, self._serial)).uninstall()
        self.status = 'disconnect'

    ###########################################################################
    # driver方法
    ###########################################################################
    @check_status
    def session_click(self, x: float, y: float):
        """
        点击事件
        :param x: x坐标
        :param y: y坐标
        :return:
        """
        return self._client.click(x, y)

    @check_status
    def session_swipe(self, fx: int, fy: int, tx: int, ty: int):
        """
        滑动事件
        :param fx: 起点x坐标
        :param fy: 起点y坐标
        :param tx: 终点x坐标
        :param ty: 终点y坐标
        :return:
        """
        return self._client.swipe(fx, fy, tx, ty)

    @check_status
    def session_system(self, key: Union[int, str]):
        """
        系统按键事件
        :param key: 按键名
        :return:
        """
        return self._client.press(key)

    @check_status
    def session_info(self):
        """
        系统信息
        :return:
        """
        return self._client.info

    @check_status
    def session_screenshot(self):
        """
        截图
        :return:
        """
        return str(base64.b64encode(self._client.screenshot(format="raw")).decode('ascii'))

    @check_status
    def session_dump(self):
        """
        获取xml控件树
        :return:
        """
        return self._android_hierarchy_to_json(self._client)

    @check_status
    def session_windows(self):
        """
        获取设备显示大小
        :return:
        """
        width, height = self._client.window_size()
        return {'width': width, 'height': height}

    def _android_hierarchy_to_json(self, d):
        """
        获取控件树
        :param d: client
        :return:
        """
        page_xml = d.dump_hierarchy(pretty=True)
        dom = xml.dom.minidom.parseString(page_xml)
        root = dom.documentElement

        def travel(node):
            """ return current node info """
            if node.attributes is None:
                return
            json_node = self._parse_uiautomator_node(node)
            json_node['_id'] = str(uuid.uuid4())
            if node.childNodes:
                children = []
                for n in node.childNodes:
                    child = travel(n)
                    if child:
                        # child["_parent"] = json_node["_id"]
                        if 'label' in json_node:
                            child["_parent"] = json_node["_id"]
                        children.append(child)
                json_node['children'] = children
            return json_node

        return travel(root)

    def _parse_uiautomator_node(self, node):
        ks = {}
        for key, value in node.attributes.items():
            key = alias.get(key, key)
            f = parsers.get(key)
            if value is None:
                ks[key] = None
            elif f:
                ks[key] = f(value)
        if '_type' in ks:
            ks['label'] = ks['_type']
        if 'bounds' in ks:
            lx, ly, rx, ry = map(int, ks.pop('bounds'))
            ks['rect'] = dict(x=lx, y=ly, width=rx - lx, height=ry - ly)
        return ks

    ###########################################################################
    # driver方法
    ###########################################################################
    @check_status
    def app_start(self, app: str):
        """
        启动app
        :param app: app包名
        :return:
        """
        return self._client.app_start(app, wait=True)

    @check_status
    def app_stop(self, app: str):
        """
        停止app
        :param app: app包名
        :return:
        """
        return self._client.app_stop(app)

    @check_status
    def app_restart(self, app: str):
        """
        重启app
        :param app: app包名
        :return:
        """
        self._client.app_stop(app)
        return self._client.app_start(app, wait=True)

    @check_status
    def app_reset(self, app: str):
        """
        重置app
        :param app: app包名
        :return:
        """
        self._client.app_clear(app)
        return self._client.app_start(app, wait=True)

    ###########################################################################
    # element方法
    ###########################################################################
    @check_status
    def element_click(self, locator):
        """
        点击控件
        :param locator: 控件定位器
        :return:
        """
        locator = AndroidLocator.create_locator(locator)
        if locator.locator_offset == (0, 0):
            if locator.locator_type == 'xpath':
                return self._client.xpath(locator.locator_value).click(5) if locator.locator_index == -1 \
                    else self._client.xpath(locator.locator_value).all()[locator.locator_index].click()
            else:
                return self._client(**locator.locator_arg).click(5) if locator.locator_index == -1 \
                    else self._client(**locator.locator_arg)[locator.locator_index].click(5)
        else:
            element_info = self.element_info(locator)
            click_x = (element_info['bounds']['left'] + element_info['bounds']['right']) / 2 + \
                      locator.locator_offset[0]
            click_y = (element_info['bounds']['top'] + element_info['bounds']['bottom']) / 2 + \
                      locator.locator_offset[1]
            return self.session_click(click_x, click_y)

    @check_status
    def element_send(self, locator, text: str):
        """
        向控件发送文字
        :param locator: 控件定位器
        :param text: 文本
        :return:
        """
        locator = AndroidLocator.create_locator(locator)
        if locator.locator_type == 'xpath':
            if locator.locator_index == -1:
                self._client.xpath(locator.locator_value).set_text(text)
            else:
                self.element_clear(locator)
                if text:
                    self._client.send_keys(text)
            ret = "success"
        else:
            ret = self._client(**locator.locator_arg).set_text(text) if locator.locator_index == -1 \
                else self._client(**locator.locator_arg)[locator.locator_index].set_text(text)
        try:
            if self.element_exists(AndroidLocator(resourceId="com.github.uiautomator:id/keyboard")):
                self.session_system("back")
            else:
                _, h = self._client.window_size()
                self.session_click(2, h / 2)
        except:
            pass
        return ret

    @check_status
    def element_clear(self, locator):
        """
        清空控件文本
        :param locator: 控件定位器
        :return:
        """
        locator = AndroidLocator.create_locator(locator)
        if locator.locator_type == 'xpath':
            self._client.set_fastinput_ime()
            self.element_click(locator)
            ret = self._client.clear_text()
        else:
            ret = self._client(**locator.locator_arg).clear_text(5) if locator.locator_index == -1 \
                else self._client(**locator.locator_arg)[locator.locator_index].clear_text(5)
        return ret

    @check_status
    def element_exists(self, locator):
        """
        判断控件是否存在
        :param locator: 控件定位器
        :return:
        """
        locator = AndroidLocator.create_locator(locator)
        if locator.locator_type == 'xpath':
            return self._client.xpath(locator.locator_value).exists if locator.locator_index == -1 \
                else len(self._client.xpath(locator.locator_value).all()) > locator.locator_index
        else:
            return self._client(**locator.locator_arg).exists() if locator.locator_index == -1 \
                else self._client(**locator.locator_arg)[locator.locator_index].exists()

    @check_status
    def element_info(self, locator):
        """
        获取控件信息
        :param locator: 控件定位器
        :return:
        """
        locator = AndroidLocator.create_locator(locator)
        if locator.locator_type == 'xpath':
            return self._client.xpath(locator.locator_value).info if locator.locator_index == -1 \
                else self._client.xpath(locator.locator_value).all()[locator.locator_index].info
        else:
            return self._client(**locator.locator_arg).info if locator.locator_index == -1 \
                else self._client(**locator.locator_arg)[locator.locator_index].info

    ###########################################################################
    # find方法
    ###########################################################################
    @check_status
    def find_element(self, locator):
        """
        查找控件
        :param locator: 控件定位器
        :return:
        """
        return self.element_exists(locator)

    @check_status
    def find_elements(self, locator):
        """
        查找控件（多个）
        :param locator: 控件定位器
        :return:
        """
        locator = AndroidLocator.create_locator(locator)
        return len(self._client.xpath(locator.locator_value).all()) if locator.locator_type == 'xpath' \
            else self._client(**locator.locator_arg).count

    @check_status
    def find_allinfo(self, locator):
        element_count = self.find_elements(locator)
        result_list = []
        for i in range(0, element_count):
            locator['index'] = i
            info = self.element_info(locator)
            if 'visibleBounds' in info:
                result_list.append({'x': info['visibleBounds']['left'], 'y': info['visibleBounds']['top'],
                                    'width': info['visibleBounds']['right'] - info['visibleBounds']['left'],
                                    'height': info['visibleBounds']['bottom'] - info['visibleBounds']['top']})
            else:
                result_list.append({'x': info['bounds']['left'], 'y': info['bounds']['top'],
                                    'width': info['bounds']['right'] - info['bounds']['left'],
                                    'height': info['bounds']['bottom'] - info['bounds']['top']})
        return result_list

    @classmethod
    def get_devices(cls):
        devices = []
        for device in adbutils.adb.iter_device():
            devices.append(device.serial)
        return devices
